<?php


namespace app\common\service\pay;

use app\common\exception\MiException;
use app\model\store\Order;
use think\facade\Log;
use WeChatPay\Builder;
use WeChatPay\BuilderChainable;
use WeChatPay\Crypto\AesGcm;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Formatter;
use WeChatPay\Util\PemUtil;

class WxpayService
{

    private BuilderChainable $builder;

    private string $appid;

    private string $merchantId;

    private string $v2key;
    private string $v3key;

    // 「微信支付平台证书」路径
    private string $platformCertificateFilePath;
    // 商户api私钥路径
    private string $merchantPrivateKeyFilePath;

    // 「商户API证书」的「证书序列号」
    private string $merchantCertificateSerial;

    // 支付回调接口
    private string $notifyUrl;

    //创建静态私有的变量保存该类对象
    static private ?WxpayService $instance = null;


    //防止使用new直接创建对象
    private function __construct($config)
    {

        $basePath = 'file://' . str_replace("\\", '/', root_path('local'));

        $this->appid = $config['appid'];

        if ($config['is_te']){  // 特约商户
            //  todo: 特约商户的配置，没有调试
//            $this->merchantId = $config['mchid'];
//            $this->merchantPrivateKeyFilePath = $basePath . $config['key_path'];
//            $this->platformCertificateFilePath = $basePath . $config['platform_cert_path'];
//            $this->merchantCertificateSerial = $config['mch_serial'];
//            $this->builder = $this->getBuilder();
        }else{  // 非特约商户
            $this->merchantId = $config['mchid'];
            $this->merchantPrivateKeyFilePath = $basePath . $config['key_path'];
            $this->platformCertificateFilePath = $basePath . $config['platform_cert_path'];
            $this->merchantCertificateSerial = $config['mch_serial'];
            $this->v3key = $config['v3key'];
            $this->v2key = $config['v2key']??'';
            $this->builder = $this->getBuilder();
        }


        $this->notifyUrl = request()->domain() . '/uniapp/public/wxpayNotify';

    }

    //防止使用clone克隆对象
    private function __clone()
    {
    }

//    获取实例
    static public function getInstance($config): ?WxpayService
    {
        //判断$instance是否是Singleton的对象，不是则创建
        if (!self::$instance instanceof self) {
            self::$instance = new self($config);
        }
        return self::$instance;
    }


    // 创建支付驱动
    private function getBuilder(): BuilderChainable
    {

    // 从本地文件中加载「商户API私钥」，「商户API私钥」会用来生成请求的签名
        $merchantPrivateKeyInstance = Rsa::from($this->merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);

    // 从本地文件中加载「微信支付平台证书」，用来验证微信支付应答的签名
        $platformCertificateFilePath = $this->platformCertificateFilePath;
        $platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);

    // 从「微信支付平台证书」中获取「证书序列号」
        $platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);

// 构造一个 APIv3 客户端实例
        return Builder::factory([
            'mchid' => $this->merchantId,
            'serial' => $this->merchantCertificateSerial,
            'privateKey' => $merchantPrivateKeyInstance,
            'certs' => [
                $platformCertificateSerial => $platformPublicKeyInstance,
            ],
        ]);

    }


    /**
     * APP支付创建订单
     * @param Order $order
     * @param $backUrl
     * @return array
     */
    public function app(Order $order, $backUrl = ''): array
    {
        try {
            if ($backUrl == '') {
                $backUrl = $this->notifyUrl;
            }

            $resp = $this->builder->chain('v3/pay/transactions/app')
                ->post(['json' => [
                    'appid' => $this->appid,
                    'mchid' => $this->merchantId,
                    'description' => $order->title,
                    'out_trade_no' => $order->code,
                    'notify_url' => $backUrl,
                    'amount' => [
                        'total' => ($order->amount) * 100,
                        'currency' => 'CNY'
                    ],
                ]]);

            return $this->formatResponse($resp);

        } catch (\Exception $e) {
            return makeErrorResult($this->formatError($e));
        }
    }


    /**
     * 小程序支付/js支付
     * @throws MiException
     */
    public function jsapi(string $outTradeNo, float $amountTotal, $openid, $desc="购买商品", string $notifyUrl =''): array|string
    {
        try {
            if ($notifyUrl == '') {
                $notifyUrl = $this->notifyUrl;
            }

            $resp = $this->builder->chain('v3/pay/transactions/jsapi')
                ->post(['json' => [
                    'appid' => $this->appid,
                    'mchid' => $this->merchantId,
                    'description' => $desc,
                    'out_trade_no' => $outTradeNo,
                    'notify_url' => $notifyUrl,
                    'amount' => [
                        'total' => (int)($amountTotal * 100),  // 这里必须是整数
                        'currency' => 'CNY'
                    ],
                    'payer' => [
                        'openid' => $openid,
                    ]
                ]]);

            $httpCode = $resp->getStatusCode();
            $httpBody = (string)$resp->getBody();
            if ($httpCode != 200) {
                throw new MiException("{$httpCode}:请求支付失败");
            }
            $bodyArr = json_decode($httpBody, true);
            $prepay = $bodyArr['prepay_id'];

            $merchantPrivateKeyInstance = Rsa::from($this->merchantPrivateKeyFilePath);
            $params = [
                'appId'     => $this->appid,
                'timeStamp' => (string)Formatter::timestamp(),
                'nonceStr'  => Formatter::nonce(),
                'package'   => 'prepay_id='.$prepay,
            ];
            $params += ['paySign' => Rsa::sign(
                Formatter::joinedByLineFeed(...array_values($params)),
                $merchantPrivateKeyInstance
            ), 'signType' => 'RSA'];
            return $params;


        } catch (\Exception $e) {
            throw new MiException($this->formatError($e));
        }
    }

    // 扫码支付
    public function scan(array $order): array|string
    {
        try {

            if (!isset($order['body'])) throw new MiException("缺少支付参数：body");
            if (!isset($order['out_trade_no'])) throw new MiException("缺少支付参数：out_trade_no");
            if (!isset($order['total_fee'])) throw new MiException("缺少支付参数：total_fee");
            if (!isset($order['spbill_create_ip'])) throw new MiException("缺少支付参数：spbill_create_ip");
            if (!isset($order['auth_code'])) throw new MiException("缺少支付参数：auth_code");

            $order['appid'] = $this->appid;
            $order['mch_id'] = $this->merchantId;
            $order['nonce_str'] = Formatter::nonce();

            $order['sign'] = $this->signMd5($order, $this->v2key);

            $resp = $this->builder->chain('pay/micropay')
                ->post(['json' => $order]);

            return $resp->getBody();

        } catch (\Exception $e) {
            return makeErrorResult($this->formatError($e));
        }
    }


    /**
     * 退款
     * @param $data
     * @return array
     * @throws MiException
     */
    public function refunds($data): array
    {
        try {

            $resp = $this->builder->chain('v3/refund/domestic/refunds')
                ->post(['json' => $data]);

            return $this->getResponseBody($resp);

        } catch (\Exception $e) {
            throw new MiException($this->formatError($e));
        }
    }

    /**
     * 商家向用户转账
     * @throws MiException
     */
    public function transfer($data): array
    {
        try {
            $resp = $this->builder->chain('v3/transfer/batches')
                ->post([
                    'json' =>array_merge([
                        'appid' => $this->appid,
                    ], $data)
                ]);

            return $this->getResponseBody($resp);

        } catch (\Exception $e) {
            throw new MiException($this->formatError($e));
        }
    }



//    public function makeAppPayParams($prepayId)
//    {
//
//        $merchantPrivateKeyFilePath = $this->merchantPrivateKeyFilePath;
//        $merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath);
//
//        // 预定义需要签名的参数
//        $params = [
//            'appid' => $this->system['wxpay_app_id'],
//            'timestamp' => (string)Formatter::timestamp(),
//            'noncestr' => Formatter::nonce(),
//            'prepayid' => $prepayId,
//        ];
//
//        // 签名
//        $params['sign'] = Rsa::sign(
//            Formatter::joinedByLineFeed(...array_values($params)),
//            $merchantPrivateKeyInstance
//        );
//
//        // 追加剩余参数
//        $params['partnerid'] = $this->system['wxpay_mch_id'];
//        $params['package'] = "Sign=WXPay";
//
//        return $params;
//
//    }

    public function signMd5 ($data, $mach_key): string
    {
        ksort($data);
        $data = array_filter($data, function ($v, $k) {
            if ($k == "sign" && $v == '' && is_array($v)) {
                return false;
            }
            return true;
        }, ARRAY_FILTER_USE_BOTH);
        $str = http_build_query($data)."&key=".$mach_key;
        //$str = urldecode($str);//处理中文乱码
        return strtoupper(md5($str));
    }


    /**
     * @throws MiException
     */
    public function notify()
    {

        try {
            Log::info("------------------------------微信支付-------------------------------------");
//            Log::info(json_encode($params));

            $signature = request()->header('Wechatpay-Signature');// 请根据实际情况获取
            $timestamp = request()->header('Wechatpay-Timestamp');// 请根据实际情况获取
            $serial = request()->header('Wechatpay-Serial');// 请根据实际情况获取
            $nonce = request()->header('Wechatpay-Nonce');// 请根据实际情况获取
            $inBody = file_get_contents('php://input');// 请根据实际情况获取，例如: file_get_contents('php://input');

            $apiv3Key = $this->v3key;  // 在商户平台上设置的APIv3密钥

            // 验证证书序列号
//            if($this->system['wxpay_mch_serial'] != $serial){
//                Log::info("---------证书序列号错误".$serial."-------".$this->system['wxpay_mch_serial']."--------");
//                return makeErrorResult("证书序列号错误");
//            }

            // 验证签名


            // 根据通知的平台证书序列号，查询本地平台证书文件，
// 假定为 `/path/to/wechatpay/inWechatpaySerial.pem`
            $platformPublicKeyInstance = Rsa::from($this->platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);

// 检查通知时间偏移量，允许5分钟之内的偏移
            $timeOffsetStatus = 300 >= abs(Formatter::timestamp() - (int)$timestamp);
            $verifiedStatus = Rsa::verify(
            // 构造验签名串
                Formatter::joinedByLineFeed($timestamp, $nonce, $inBody),
                $signature,
                $platformPublicKeyInstance
            );
            if (!$timeOffsetStatus || !$verifiedStatus) {
                Log::error("---------签名验证失败-------");
                throw new MiException("签名验证失败");
            }

            // 转换通知的JSON文本消息为PHP Array数组
            $inBodyArray = (array)json_decode($inBody, true);
            // 使用PHP7的数据解构语法，从Array中解构并赋值变量
            [
                'event_type' => $eventType,
                'resource' => [
                    'ciphertext' => $ciphertext,
                    'nonce' => $nonce,
                    'associated_data' => $aad
                ]
            ] = $inBodyArray;
            // 加密文本消息解密
            $inBodyResource = AesGcm::decrypt($ciphertext, $apiv3Key, $nonce, $aad);
            // 把解密后的文本转换为PHP Array数组
            $params = (array)json_decode($inBodyResource, true);
            Log::info("----解密后的请求信息----". json_encode($params));

            // 验证商户号
            if ($this->merchantId != $params['mchid']) {
                Log::error("-----商户号验证失败------");
                throw new MiException("商户号验证失败");
            }

            if ($eventType == 'TRANSACTION.SUCCESS'){    // 退款成功通知
                Log::info("-------------付款成功----------");

            } elseif ($eventType == 'REFUND.SUCCESS'){    // 退款成功通知
                Log::info("-------------退款成功----------");

            }else{
                throw new MiException("------失败通知：{$eventType}----------");
            }
            // 将交易时间转为php时间戳
            $params['success_time'] = strtotime($params['success_time']);
            return $params;

        } catch (\Exception $e) {
            Log::error("----代码错误----" . $e->getMessage() . PHP_EOL);
            throw new MiException($e->getMessage());
        }

    }


    /**
     * @throws MiException
     */
    public function getResponseBody($resp): array
    {

        $httpCode = $resp->getStatusCode();
        $httpBody = (string)$resp->getBody();
        if ($httpCode != 200) {
            throw new MiException("{$httpCode}:请求支付失败");
        }
        return json_decode($httpBody, true);
    }


    private function formatError(\Exception $e): string
    {
        // 进行错误处理
        $err = $e->getMessage() . PHP_EOL;;
        if ($e instanceof \GuzzleHttp\Exception\RequestException && $e->hasResponse()) {

            $r = $e->getResponse();
            $err .= $r->getStatusCode() . ' ' . $r->getReasonPhrase() . PHP_EOL;
            $err .= $r->getBody() . PHP_EOL . PHP_EOL . PHP_EOL;

        }
        $err .= $e->getTraceAsString() . PHP_EOL;
        return $err;

    }

}
